import type {
  AutomationStep,
  BookingContext,
  BookingResult,
  IAutomationEngine,
  StepResult,
  TimeslotInfo,
} from '@shared/types/'
import { getConfig } from '../config/index'
import {
  createTrackedContextLogger,
  generateTraceId,
  log,
  PerformanceLogger,
} from '../logger/index'
import { ensureRunDirectory } from '../logger/recorders'
import { UserLogger } from '../logger/user-logger'
import { getPageParser } from '../shared/page-parser'
import { BookingBrowserManager } from './booking-browser-manager'
import { analyzeCalendarSummary, buildMonthsRange, gotoMonth as gotoCalMonth, parseDDMMYYYY, readCalendar as readCal } from './helpers/calendar'
import { generateRandomRelease } from './helpers/mock-release'

// =====================
// 基础站点首选缓存（避免每次都先试 abs1 造成时间浪费）
// - 仅进程内缓存，应用退出后重置
// - 命中成功则提升为首选；TTL 到期或失败时自然回落
// =====================
let preferredBase: { url: string, until: number } | null = null
const PREFERRED_BASE_TTL_MS = 15 * 60 * 1000 // 15分钟
function reorderBasesWithPreference(bases: string[]): string[] {
  if (!preferredBase)
    return bases
  if (Date.now() > preferredBase.until) {
    preferredBase = null
    return bases
  }
  const idx = bases.indexOf(preferredBase.url)
  if (idx > 0) {
    const arr = bases.slice()
    arr.splice(idx, 1)
    arr.unshift(preferredBase.url)
    return arr
  }
  return bases
}
function promotePreferredBase(url: string): void {
  try {
    preferredBase = { url, until: Date.now() + PREFERRED_BASE_TTL_MS }
  }
  catch {}
}

/**
 * 预约引擎（Booking 模式）
 *
 * 核心思路：使用 Playwright 通过 JS 方法注入 + 表单强制提交的方式完成整个预约流程。
 * 与 automation 模块的区别：
 * - automation：无代理，直接 page.goto 跳转各步骤。
 * - booking：有代理，通过调用页面自带 JS 函数（如 onClickStartApplication/onClickDISevType/onClickContinue）
 *   保持会话状态与隐藏域连续性；每次调用前强制重置 isSubmitted() 防止重复提交拦截。
 *
 * 主要步骤：
 * 1. 访问首页（cmdShowHome）
 * 2. 同意条款（调用 onClickStartApplication 进入个人信息页）
 * 3. 填写个人信息（选择 DI 服务 + 填充字段 + 准备隐藏域）
 * 4. 身份验证（调用 onClickContinue 提交表单；分支处理电话补填/事务处选择）
 * 5. 查询可用日期（扫描日历，支持翻月，最多3个月）
 * 6. 选择时段（调用 onClickDate 进入时段页，解析并选择时段）
 * 7. 确认预约（调用 onClickTimeslot 进入确认页，调用 onClickSubmit 提交）
 *
 * 关键策略：
 * - 每次 evaluate 调用页面函数前，强制重置 isSubmitted 标记为 false，避免"已提交"拦截。
 * - 每次 evaluate 提交后，等待 URL 变化/load/readyState=complete/特定元素出现，确保页面完全加载。
 * - 所有步骤优先调用页面自带 JS 函数，若无则手动构造隐藏域并 submit，确保流程鲁棒。
 * - 增加详细耗时日志，方便定位性能瓶颈。
 */
export class BookingEngineForBooking implements IAutomationEngine {
  private currentStep = '' // 当前步骤名称
  private progress = 0 // 当前进度（0-100）
  private cancelled = false // 是否已取消
  private config = getConfig() // 全局配置
  private parser = getPageParser() // 页面解析器
  private traceId: string = '' // 本次预约的追踪 ID
  private sessionId: string = '' // 本次会话 ID
  private contextLogger: ReturnType<typeof createTrackedContextLogger> | null = null // 上下文日志记录器
  // 节流：记录关键操作上次时间，避免触发"频繁访问"（如日历翻月）
  private lastActionAt: Record<string, number> = {}
  private browser: BookingBrowserManager | null = null

  /**
   * 执行完整预约流程
   * @param context 预约上下文（包含用户信息、首选日期时间、选项等）
   * @returns 预约结果（成功返回参考编号，失败返回错误信息）
   */
  async execute(context: BookingContext): Promise<BookingResult> {
    this.cancelled = false
    this.progress = 0
    this.traceId = generateTraceId()
    this.sessionId = `booking-${Date.now()}-${Math.random().toString(36).substr(2, 6)}`
    this.jobId = context.options?.jobId // 从 options 获取 jobId

    // 初始化上下文日志记录器（所有日志会自动带上 sessionId/userName/passport）
    this.contextLogger = createTrackedContextLogger({
      component: 'BookingEngine',
      sessionId: this.sessionId,
      userName: context.user_info.license_owner_name,
      passport: context.user_info.passport_number,
    }, this.traceId)

    // 记录预约流程开始（文件日志）
    log.booking.start(this.sessionId, context.user_info, { traceId: this.traceId })

    // 记录用户日志（UI展示）
    if (this.jobId) {
      await UserLogger.step(this.jobId, 'startBooking', 5)
    }

    PerformanceLogger.start(`booking-${this.sessionId}`)

    const browser = new BookingBrowserManager()
    this.browser = browser

    try {
      await this.updateProgress(5, '启动浏览器', context)
      this.contextLogger.info('🚀 启动浏览器', {
        headless: context.options?.headless ?? true,
        timeout: context.options?.timeout ?? 30000,
      })

      // 初始化本次运行目录（不再写入全局环境变量），并绑定到浏览器管理器实例
      const runDir = await ensureRunDirectory('booking', {
        name: context.user_info.license_owner_name,
        passportTail: (context.user_info.passport_number || '').slice(-4) || 'xxxx',
      }, false)
      try { browser.setRunDirectory(runDir) }
      catch {}

      await browser.launch({
        headless: context.options?.headless ?? true,
        timeout: context.options?.timeout ?? 30000,
      })

      const page = browser.getCurrentPage()
      if (!page)
        throw new Error('页面未初始化')

      // 执行所有步骤（按顺序）
      const steps = this.createBookingSteps()
      for (const step of steps) {
        // 检查是否已取消
        if (this.cancelled)
          throw new Error('预约流程已被取消')

        // 记录步骤开始（附上当前 URL，便于定位“停在首页不动”的任务）
        try {
          const curUrl = browser.getCurrentPage()?.url()
          log.booking.step(this.sessionId, step.name, 'start', undefined, { traceId: this.traceId, currentUrl: curUrl })
        }
        catch {
          log.booking.step(this.sessionId, step.name, 'start', undefined, { traceId: this.traceId })
        }
        PerformanceLogger.start(`step-${step.name}`)

        // 执行步骤（包含重试逻辑）
        const result = await this.executeStep(step, context)
        const stepDuration = PerformanceLogger.end(`step-${step.name}`)

        // 步骤失败则终止流程（"无可用日期"降级为 warn 级别记录）
        if (!result.success) {
          const errMsg = String(result.error || '')
          const isNoDate = /没有可用.*日期|无可用日期/.test(errMsg)

          // 文件日志
          log.booking.step(this.sessionId, step.name, isNoDate ? 'warn' : 'error', stepDuration, {
            traceId: this.traceId,
            error: result.error,
          })

          // 用户日志（UI展示）
          if (this.jobId) {
            const error = result.error ? (typeof result.error === 'string' ? new Error(result.error) : result.error) : new Error('步骤执行失败')
            await UserLogger.error(this.jobId, error, step.name)
          }

          throw new Error(`步骤失败: ${step.name} - ${result.error}`)
        }

        // 记录步骤成功
        log.booking.step(this.sessionId, step.name, 'success', stepDuration, { traceId: this.traceId })

        if (this.config.logging.save_html && result.html_saved) {
          this.contextLogger!.debug(`HTML 已保存: ${result.html_saved}`)
        }
      }

      // 所有步骤成功，记录总耗时和结果
      const totalDuration = PerformanceLogger.end(`booking-${this.sessionId}`)
      const bookingResult = {
        success: true,
        reference_number: (context.session_data as any)?.reference_number,
        // 记录预约日期和时间（便于修复脚本从日志中提取）
        selected_date: context.selected_date,
        selected_timeslot: context.selected_timeslot?.display_time,
        message: '预约成功完成',
      }

      log.booking.end(this.sessionId, true, bookingResult, totalDuration, { traceId: this.traceId })

      // 记录用户日志（UI展示）
      if (this.jobId) {
        await UserLogger.step(this.jobId, 'complete', 100, '预约完成')
      }

      return bookingResult
    }
    catch (e: unknown) {
      // 流程失败，记录错误和总耗时
      const message = e instanceof Error ? e.message : String(e)
      const totalDuration = PerformanceLogger.measure(`booking-${this.sessionId}`) || 0

      const failureResult = { success: false, error_code: 'BOOKING_FAILED', message }
      log.booking.end(this.sessionId, false, failureResult, totalDuration, { traceId: this.traceId })

      // 记录用户日志（UI展示）
      if (this.jobId && e instanceof Error) {
        await UserLogger.error(this.jobId, e, '预约流程')
      }

      // 记录系统错误（用于异常分析）
      if (e instanceof Error) {
        log.system.error(e, 'BookingEngine.execute', {
          traceId: this.traceId,
          sessionId: this.sessionId,
          user_action: '用户发起预约流程',
          recovery_suggestion: '请检查网络连接和用户信息，稍后重试',
        })
      }
      else {
        this.contextLogger?.error('预约流程执行失败', { error: message })
      }

      return failureResult
    }
    finally {
      // 无论成功失败，都要关闭浏览器释放资源
      try {
        await browser.close()
        this.contextLogger?.info('浏览器已关闭')
      }
      catch (err) {
        this.contextLogger?.warn('关闭浏览器时出现问题', { error: String(err) })
      }
    }
  }

  /**
   * 执行单个步骤（带超时控制和重试机制）
   * @param step 步骤定义（包含名称、执行函数、超时时间、重试配置）
   * @param context 预约上下文
   * @returns 步骤执行结果
   */
  async executeStep(step: AutomationStep, context: BookingContext): Promise<StepResult> {
    const startTime = Date.now()
    try {
      // 设置步骤超时时间（优先级：步骤配置 > 全局配置 > 默认30s）
      const timeout = step.timeout || context.options?.timeout || 30000
      // 执行步骤，超时则抛出异常
      const result = await Promise.race([
        step.execute(context),
        new Promise<StepResult>((_, reject) => setTimeout(() => reject(new Error('步骤执行超时')), timeout)),
      ])
      const duration = Date.now() - startTime
      this.contextLogger?.debug(`步骤执行完成`, {
        step: step.name,
        duration,
        timeout,
        durationText: `${duration}ms`,
        timeoutText: `${timeout}ms`,
      })
      return result
    }
    catch (e: unknown) {
      if (this.cancelled)
        return { success: false, error: '预约流程已被取消' }
      this.contextLogger?.error(`步骤执行失败`, { step: step.name, error: String(e) })

      // 如果配置了重试，则进行重试
      if (step.retry && step.retry.max_attempts > 1) {
        this.contextLogger?.info(`开始重试步骤`, {
          step: step.name,
          remainingAttempts: step.retry.max_attempts - 1,
        })

        for (let attempt = 1; attempt < step.retry.max_attempts; attempt++) {
          // 等待指定延时后重试（可中断）
          if (this.cancelled)
            return { success: false, error: '预约流程已被取消' }
          await this.sleepCancellable(step.retry!.delay_ms)
          try {
            PerformanceLogger.start(`retry-${step.name}-${attempt}`)
            const retryResult = await step.execute(context)
            const retryDuration = PerformanceLogger.end(`retry-${step.name}-${attempt}`)

            this.contextLogger?.info(`步骤重试成功`, {
              step: step.name,
              attempt,
              duration: retryDuration,
              durationText: `${retryDuration}ms`,
            })
            return retryResult
          }
          catch (err) {
            this.contextLogger?.warn(`步骤重试失败`, {
              step: step.name,
              attempt,
              error: String(err),
            })
          }
        }
      }

      const message = e instanceof Error ? e.message : String(e)
      return { success: false, error: message }
    }
  }

  /**
   * 获取当前进度（0-100）
   */
  getProgress(): number { return this.progress }

  /**
   * 获取当前步骤名称
   */
  getCurrentStep(): string { return this.currentStep }

  /**
   * 取消预约流程
   */
  async cancel(): Promise<void> {
    this.cancelled = true
    // 优雅但尽快地中断：立即关闭浏览器，打断任何进行中的导航/等待
    try {
      const bm = this.browser
      this.browser = null
      if (bm)
        await bm.close()
    }
    catch {}
    this.contextLogger?.info('预约流程已请求取消', { sessionId: this.sessionId })
  }

  /**
   * 若已取消则立即抛出，避免继续进行当前步骤的恢复/重试逻辑
   */
  private throwIfCancelled(): void {
    if (this.cancelled)
      throw new Error('预约流程已被取消')
  }

  /** Job ID（用于UI日志记录） */
  private jobId: string | undefined

  /**
   * 更新进度和当前步骤
   */
  private async updateProgress(progress: number, step: string, context?: BookingContext): Promise<void> {
    this.progress = progress
    this.currentStep = step

    // 如果有 jobId，使用 UserLogger 记录用户友好日志
    if (this.jobId) {
      await UserLogger.step(this.jobId, step, progress)
    }

    // 调用原有的 onProgress 回调（如果存在）
    if (context?.options?.onProgress) {
      context.options.onProgress({
        progress,
        step,
        timestamp: Math.floor(Date.now() / 1000),
      })
    }
  }

  /**
   * 创建预约步骤列表
   * 每个步骤包含：名称、描述、执行函数、重试配置
   * 重试策略：
   * - 访问首页：3次，间隔2s（网络不稳定）
   * - 同意条款：2次，间隔1s（偶发导航失败）
   * - 填写个人信息：2次，间隔1.5s（表单加载慢）
   * - 身份验证：3次，间隔2s（可能需补填电话/选事务处）
   * - 查询可用日期：5次，间隔3s（日历翻月可能触发限流）
   * - 选择时段：3次，间隔1.5s（时段解析可能失败）
   * - 确认预约：2次，间隔2s（最终提交偶发超时）
   */
  private createBookingSteps(): AutomationStep[] {
    return [
      { name: '访问首页', description: '打开预约系统首页', execute: async (c) => {
        await this.updateProgress(10, '访问首页', c)
        return await this.stepNavigateHome(c)
      }, retry: { max_attempts: 3, delay_ms: 2000 } },
      { name: '同意条款', description: '同意服务条款并继续', execute: async (c) => {
        await this.updateProgress(20, '同意条款', c)
        return await this.stepAgreeTerms(c)
      }, timeout: 70000, retry: { max_attempts: 2, delay_ms: 1000 } },
      { name: '填写个人信息', description: '填写用户个人信息表单', execute: async (c) => {
        await this.updateProgress(35, '填写个人信息', c)
        return await this.stepFillPersonalInfo(c)
      }, retry: { max_attempts: 2, delay_ms: 1500 } },
      { name: '身份验证', description: '提交身份验证信息', execute: async (c) => {
        await this.updateProgress(50, '身份验证', c)
        return await this.stepVerifyIdentity(c)
      }, retry: { max_attempts: 3, delay_ms: 2000 } },
      { name: '查询可用日期', description: '获取可预约的日期列表', execute: async (c) => {
        await this.updateProgress(65, '查询可用日期', c)
        return await this.stepQueryAvailableDates(c)
      }, retry: { max_attempts: 5, delay_ms: 3000 } },
      { name: '选择时段', description: '选择合适的预约时段', execute: async (c) => {
        await this.updateProgress(80, '选择时段', c)
        return await this.stepSelectTimeslot(c)
      }, retry: { max_attempts: 3, delay_ms: 1500 } },
      { name: '确认预约', description: '确认预约信息并提交', execute: async (c) => {
        await this.updateProgress(95, '确认预约', c)
        return await this.stepConfirmBooking(c)
      }, retry: { max_attempts: 2, delay_ms: 2000 } },
    ]
  }

  /**
   * 步骤1：访问首页
   * URL: ?cmd=cmdShowHome
   * 成功标志：页面包含"開始預約"文本
   * 失败处理：轮询所有 base_urls，全失败则直连重试
   */
  private async stepNavigateHome(context: BookingContext): Promise<StepResult> {
    const browser = this.browser!
    this.contextLogger?.info('开始访问首页', { url: this.config.network.base_url })

    // 单次尝试访问指定入口
    const attempt = async (baseUrl: string): Promise<StepResult> => {
      this.throwIfCancelled()
      const page = browser.getCurrentPage()!
      try {
        // 直接命中首页入口（与 automation 一致）
        const homeUrl = `${baseUrl}?cmd=cmdShowHome`
        this.contextLogger?.debug('导航到首页', { url: homeUrl })
        await page.goto(homeUrl, { waitUntil: 'domcontentloaded' })
        await this.randomDelay(this.config.booking.human_like_delays.page_view_delay)

        const html = await page.content()
        const pageType = this.parser.identifyPageType(html, page.url())
        this.contextLogger?.debug('页面类型识别完成', { pageType, currentUrl: page.url() })

        const errorResult = this.parser.detectError(html)
        if (errorResult.success && errorResult.data)
          return { success: false, error: `页面错误: ${errorResult.data.message} (${errorResult.data.code})` }

        let htmlPath: string | undefined
        if (this.config.logging.save_html) {
          const snapshotResult = await browser.saveSnapshot('01_home')
          htmlPath = snapshotResult.htmlPath
        }

        const hasStartButton = html.includes('開始預約') || html.includes('开始预约')
        this.contextLogger?.info('首页访问成功', { pageType, hasStartButton })
        try { promotePreferredBase(baseUrl) }
        catch {}
        return { success: true, page_type: pageType, html_saved: htmlPath }
      }
      catch (e: unknown) {
        const message = e instanceof Error ? e.message : String(e)
        // 若在导航/读取期间被取消，直接返回取消错误
        if (this.cancelled)
          return { success: false, error: '预约流程已被取消' }
        return { success: false, error: message }
      }
    }

    // 入口轮询：尝试所有配置的入口（abs1/abs2/abs...）+ 失败则轮换代理重启
    const rawBases = Array.isArray((this.config as any)?.network?.base_urls) && (this.config as any).network.base_urls.length > 0
      ? (this.config as any).network.base_urls as string[]
      : [this.config.network.base_url]
    const bases = reorderBasesWithPreference(rawBases)

    const maxAttempts = Math.max(1, Number((this.config as any)?.proxy?.max_proxy_attempts ?? 2))

    for (let attemptIdx = 0; attemptIdx < maxAttempts; attemptIdx++) {
      // 并发下不同任务可能同时在访问首页，给每次尝试加上轻微抖动，减少同秒洪峰
      await this.randomDelay({ min: 0.15, max: 0.35 })
      for (const base of bases) {
        this.throwIfCancelled()
        const res = await attempt(base)
        if (res.success)
          return res
        if (this.cancelled)
          return { success: false, error: '预约流程已被取消' }
      }

      // 失败：拉黑当前代理并旋转一个新代理重启
      try {
        // 将当前代理加入黑名单（TTL）
        try { (browser as any).banCurrentProxy?.('home navigation failed') }
        catch {}

        this.contextLogger?.info('首页访问失败，尝试轮换代理重启', { 尝试序号: attemptIdx + 1, 最大次数: maxAttempts })
        this.throwIfCancelled()
        await browser.close()
        // 关闭后稍作冷却，避免 newPage 仍命中 Target closed
        await new Promise(r => setTimeout(r, 200))
        this.throwIfCancelled()
        await browser.launch({
          headless: context.options?.headless ?? true,
          timeout: context.options?.timeout ?? 30000,
        } as any)
        // 重启后按首选缓存重新排序 bases（若上一次成功的是 abs2，下轮优先 abs2）
        const again = reorderBasesWithPreference(bases)
        for (const base of again) {
          this.throwIfCancelled()
          const res2 = await attempt(base)
          if (res2.success)
            return res2
          if (this.cancelled)
            return { success: false, error: '预约流程已被取消' }
        }
      }
      catch (e) {
        const msg = e instanceof Error ? e.message : String(e)
        this.contextLogger?.warn('轮换代理重启失败', { 错误: msg })
      }
    }

    // 全部轮换失败后，最后再尝试一次直连回退
    try {
      this.contextLogger?.info('所有代理尝试均失败，最后尝试直连回退')
      this.throwIfCancelled()
      await browser.close()
      await new Promise(r => setTimeout(r, 200))
      this.throwIfCancelled()
      await browser.launch({
        headless: context.options?.headless ?? true,
        timeout: context.options?.timeout ?? 30000,
        disableProxy: true,
      } as any)
      const res = await attempt(reorderBasesWithPreference(bases)[0])
      if (res.success)
        return res
      return { success: false, error: `访问首页失败: ${res.error || '未知错误'}` }
    }
    catch (e: unknown) {
      if (this.cancelled)
        return { success: false, error: '预约流程已被取消' }
      const message = e instanceof Error ? e.message : String(e)
      this.contextLogger?.error('直连回退失败', { error: message })
      return { success: false, error: `访问首页失败: ${message}` }
    }
  }

  /**
   * 步骤2：同意条款并进入个人信息页
   * 当前页：条款页（cmdStartNewApplication）
   * 操作：勾选条款 + 调用 onClickStartApplication()
   * 目标页：个人信息页（cmdInputPersonalInfo）
   * 关键：必须先重置 isSubmitted() 函数，否则会被"已提交"拦截导致表单不提交、URL不变化
   */
  private async stepAgreeTerms(_context: BookingContext): Promise<StepResult> {
    this.throwIfCancelled()
    const browser = this.browser!
    let page = browser.getCurrentPage()
    if (!page) {
      // 页面对象为空：新建页面并回到首页，避免后续对 null.page.url 的读取报错
      this.contextLogger?.warn('页面对象为空，尝试新建页面并回到首页')
      try {
        page = await browser.newPage()
        await page.goto(`${this.config.network.base_url}?cmd=cmdShowHome`, { waitUntil: 'domcontentloaded', timeout: 30000 })
        await this.randomDelay({ min: 0.2, max: 0.5 })
      }
      catch {}
      if (!page)
        return { success: false, error: '页面未初始化，无法继续同意条款' }
    }
    this.contextLogger?.info('开始处理条款页面')
    try {
      // 简化版：先确保到达条款页，再调用条款页的开始方法进入个人信息页
      const stepStartTime = Date.now()

      // 快速短路：若已在个人信息页，直接返回成功，避免重复点击与等待
      try {
        if (page.url().includes('cmdInputPersonalInfo')) {
          const html0 = await page.content()
          const pageType0 = this.parser.identifyPageType(html0, page.url())
          this.contextLogger?.info('✅ 已在个人信息页(跳过条款提交)')
          return { success: true, page_type: pageType0 }
        }
      }
      catch {}

      // A) 若仍在首页，则点击首页“开始预约”进入条款页
      if (!page.url().includes('cmdStartNewApplication')) {
        await page.evaluate(() => {
          try { (window as any).isSubmitted = function () { return false } }
          catch {}
          if (typeof (window as any).onClickStartApplication === 'function') {
            (window as any).onClickStartApplication() // 首页：跳转到条款页
          }
        })
        await page.waitForFunction(() => window.location.href.includes('cmdStartNewApplication'), { timeout: 30000 })
        await page.waitForLoadState('domcontentloaded', { timeout: 30000 }).catch(() => {})
      }

      // B) 条款页：勾选复选框，直接调用 onClickStartApplication 进入个人信息页（避免 doOutput 分支到查询/更改流程）
      const t1 = Date.now()
      await page.evaluate(() => {
        try { (window as any).isSubmitted = function () { return false } }
        catch {}
        try {
          const box = (document.getElementById('checkbox') as HTMLInputElement | null)
            || (document.querySelector('input[type="checkbox"][name="checkbox"]') as HTMLInputElement | null)
          if (box) {
            box.checked = true
            try { box.dispatchEvent(new Event('change', { bubbles: true })) }
            catch {}
          }
          // 可选：标记 agree=Y
          const frm = document.querySelector('form[name="frmAppointment"]') as HTMLFormElement | null
          if (frm) {
            let hl = frm.querySelector('input[name="agree"]') as HTMLInputElement | null
            if (!hl) { hl = document.createElement('input'); hl.type = 'hidden'; hl.name = 'agree'; frm.appendChild(hl) }
            hl.value = 'Y'
          }
        }
        catch {}
        if (typeof (window as any).onClickStartApplication === 'function') {
          (window as any).onClickStartApplication() // 条款页：跳转个人信息页
        }
        else {
          const frm = document.querySelector('form[name="frmAppointment"]') as HTMLFormElement | null
          if (frm) {
            frm.action = '/tdab2/tdabs_external/AdminServlet_tchinese?cmd=cmdInputPersonalInfo'
            frm.submit()
          }
        }
      })

      // C) 等待进入个人信息页
      // 等待进入个人信息页：先看 URL，再看表单挂载；若两者都没马上出现，退而求其次等待 body 存活
      const urlBecamePersonal = await page.waitForFunction(() => window.location.href.includes('cmdInputPersonalInfo'), { timeout: 20000 }).then(() => true).catch(() => false)
      if (!urlBecamePersonal)
        await page.waitForSelector('form[name="frmAppointment"]', { timeout: 15000 }).catch(() => {})
      const urlChangeTime = Date.now() - t1
      // 精简等待：DOM 完成即可，避免长时间 complete 卡顿
      await page.waitForLoadState('domcontentloaded', { timeout: 20000 }).catch(() => {})
      await this.randomDelay({ min: 0.2, max: 0.5 })

      const totalWaitTime = Date.now() - stepStartTime
      this.contextLogger?.debug(`⏱️ 提交→进入个人信息页耗时`, { urlChange: `${urlChangeTime}ms`, total: `${totalWaitTime}ms` })

      const html = await page.content()
      const pageType = this.parser.identifyPageType(html, page.url())

      const errorResult = this.parser.detectError(html)
      if (errorResult.success && errorResult.data)
        return { success: false, error: `进入个人信息页错误: ${errorResult.data.message}` }

      let htmlPath: string | undefined
      if (this.config.logging.save_html) {
        const snapshotResult = await browser.saveSnapshot('02_after_terms')
        htmlPath = snapshotResult.htmlPath
      }

      this.contextLogger?.info('✅ 已进入个人信息页', { pageType, url: page.url() })
      return { success: true, page_type: pageType, html_saved: htmlPath }
    }
    catch (e: unknown) {
      const message = e instanceof Error ? e.message : String(e)
      if (e instanceof Error) {
        log.system.error(e, 'BookingEngine.stepAgreeTerms', {
          traceId: this.traceId,
          sessionId: this.sessionId,
          user_action: '用户尝试同意服务条款',
          recovery_suggestion: '检查网络连接，稍后重试',
        })
      }
      this.contextLogger?.error('❌ 同意条款失败', { 错误: message })
      return { success: false, error: `同意条款失败: ${message}` }
    }
  }

  /**
   * 步骤3：选择服务及输入资料（步骤一）
   * 当前页：个人信息页（cmdInputPersonalInfo）- 由上一步已进入
   * 操作：
   *   1. 调用 onClickDISevType('DI') 选择"免试签发"服务
   *   2. 填充可见输入框：diPassportNumber/txtLicenseNumber/txtLicenseOwnerName/txtEmailAddress
   *   3. 勾选签发国家/机关单选框：input[name="issuingCountry"]/input[name="issuingAuthority"]
   *   4. 填充隐藏域（对齐站点 onClickContinue 中 DI 分支）：
   *      - storedAppointmentServiceType = 'DI'
   *      - storedAppointmentLicenseType = 'P'
   *      - storedAppointmentLicenseNumber = 护照号
   *      - storedAppointmentLicenseNumber2 = 驾照号
   *      - storedAppointmentIssuingCountry/Authority = CN/GD
   *      - storedAppointmentLicenseOwnerName = 姓名
   *      - storedAppointmentConfirmationEmail/ReminderEmail = 邮箱
   *      - sendReminder = 'Y'
   *      - storedAppointmentPhoneNumber = 电话 或 'N/A'
   *      - transferOfOwnership = 'N', inputType = 'N', storedAppointmentExpiryDate = '11-11-1901'
   * 快照：02b_service_DI（DI选中后）、03_personal_info_filled（字段填充完成后）
   */
  private async stepFillPersonalInfo(context: BookingContext): Promise<StepResult> {
    this.throwIfCancelled()
    const browser = this.browser!
    const page = browser.getCurrentPage()!
    const userInfo = context.user_info
    this.contextLogger?.info('开始"选择服务及输入资料"（步骤一）', {
      passport: userInfo.passport_number,
      license: userInfo.license_number,
      name: userInfo.license_owner_name,
    })
    try {
      // 页面已由上一步进入个人信息页（cmdInputPersonalInfo），等待表单完全就绪
      await page.waitForSelector('form[name="frmAppointment"]', { timeout: 20000, state: 'attached' as any })
      await this.randomDelay({ min: 0.4, max: 0.8 })

      // JS 注入：选择 DI 服务 + 填充所有字段 + 准备隐藏域
      const tf0 = Date.now()
      this.contextLogger?.debug('📝 开始注入：选择DI服务 + 填充字段')
      await page.evaluate((payload) => {
        const frm = document.querySelector('form[name="frmAppointment"]') as HTMLFormElement | null
        const setHidden = (name: string, val: string) => {
          if (!frm)
            return
          let el = frm.querySelector(`input[name="${name}"]`) as HTMLInputElement | null
          if (!el) {
            el = document.createElement('input')
            el.type = 'hidden'
            el.name = name
            frm.appendChild(el)
          }
          el.value = val
        }
        const setValue = (id: string, val: string) => {
          const el = document.getElementById(id) as HTMLInputElement | null
          if (el)
            el.value = val
        }

        // 1. 选择 DI 服务（优先函数，否则勾选）
        try {
          if (typeof (window as any).onClickDISevType === 'function') {
            (window as any).onClickDISevType('DI')
          }
          else {
            const diBox = document.getElementById('DICheckBox') as HTMLInputElement | null
            if (diBox) {
              diBox.checked = true
            }
          }
        }
        catch {}

        // 2. 填充可见输入
        setValue('diPassportNumber', payload.passport)
        setValue('txtLicenseNumber', payload.license)
        setValue('txtLicenseOwnerName', payload.name)
        setValue('txtEmailAddress', payload.email)

        // 3. 选择签发国家/机关（单选框）
        const countryRadio = document.querySelector(`input[name="issuingCountry"][value="${payload.issuingCountry}"]`) as HTMLInputElement | null
        if (countryRadio) {
          countryRadio.checked = true
          // 触发 toggleDivCNLicenseName 以显示/隐藏"中文姓名/发证机关"
          try {
            if (typeof (window as any).toggleDivCNLicenseName === 'function')
              (window as any).toggleDivCNLicenseName()
          }
          catch {}
        }
        const authRadio = document.querySelector(`input[name="issuingAuthority"][value="${payload.issuingAuthority}"]`) as HTMLInputElement | null
        if (authRadio)
          authRadio.checked = true

        // 4. 填充 frmAppointment 隐藏域（对齐 onClickContinue 中 DI 分支的赋值逻辑）
        setHidden('storedAppointmentServiceType', 'DI')
        setHidden('storedAppointmentLicenseType', 'P')
        setHidden('storedAppointmentLicenseNumber', payload.passport)
        setHidden('storedAppointmentLicenseNumber2', payload.license)
        setHidden('storedAppointmentExpiryDate', '11-11-1901')
        setHidden('storedAppointmentRegMarkNumber', '')
        setHidden('storedAppointmentShortRegMarkNumber', '')
        setHidden('storedAppointmentIssuingCountry', payload.issuingCountry)
        setHidden('storedAppointmentIssuingAuthority', payload.issuingAuthority)
        setHidden('storedAppointmentLicenseOwnerName', payload.name)
        setHidden('storedAppointmentReminderFaxNumber', '')
        setHidden('storedAppointmentReminderEmail', payload.email)
        setHidden('storedAppointmentConfirmationFaxNumber', '')
        setHidden('storedAppointmentConfirmationEmail', payload.email)
        setHidden('sendReminder', 'Y')
        setHidden('storedAppointmentPhoneNumber', payload.phone || 'N/A')
        setHidden('transferOfOwnership', 'N')
        setHidden('inputType', 'N')
      }, {
        passport: userInfo.passport_number,
        license: userInfo.license_number,
        name: userInfo.license_owner_name,
        email: userInfo.email,
        phone: userInfo.phone_number || '',
        issuingCountry: userInfo.issuing_country,
        issuingAuthority: userInfo.issuing_authority,
      })

      if (this.config.logging.save_html) {
        await browser.saveSnapshot('02b_service_DI')
        await browser.saveSnapshot('03_personal_info_filled')
      }

      const fillTime = Date.now() - tf0
      this.contextLogger?.info(`✅ 步骤一完成（选择服务及输入资料）`, { 耗时: `${fillTime}ms`, 服务: 'DI', 护照: userInfo.passport_number })
      return { success: true, page_type: 'personal_info', html_saved: undefined }
    }
    catch (e: unknown) {
      const message = e instanceof Error ? e.message : String(e)
      this.contextLogger?.error('❌ 选择服务及填写资料失败', { 错误: message })
      return { success: false, error: `填写个人信息失败: ${message}` }
    }
  }

  /**
   * 步骤4：身份验证（提交步骤一表单）
   * 当前页：个人信息页（cmdInputPersonalInfo）- 已由上一步填充完毕
   * 操作：调用 onClickContinue() 提交表单到身份验证接口
   * 目标页：可能的分支页面：
   *   - 电话补填页（SC-512-1-044）：需填 txtPhoneNumber 再 onClickContinue()
   *   - 选择牌照事务处页（SC-512-1-004）：需选 office 再 onClickSelectOffice/onChangeOffice()
   *   - 日历页（cmdGetCalendarAction）：直接进入
   * 关键：onClickContinue() 内部会校验表单并填充隐藏域，我们在上一步已准备好所有字段
   */
  private async stepVerifyIdentity(context: BookingContext): Promise<StepResult> {
    this.throwIfCancelled()
    const browser = this.browser!
    const page = browser.getCurrentPage()!
    const userInfo = context.user_info
    this.contextLogger?.info('开始身份验证阶段（提交步骤一表单）')
    try {
      // 调用 onClickContinue 提交表单到 cmdCheckIdentifierAction
      const verifyStartTime = Date.now()
      await page.evaluate(() => {
        // 【关键】重置 isSubmitted 标记，确保能成功提交
        try {
          (window as any).isSubmitted = function () { return false }
        }
        catch {}

        if (typeof (window as any).onClickContinue === 'function') {
          (window as any).onClickContinue()
        }
        else {
          const frm = document.querySelector('form[name="frmAppointment"]') as HTMLFormElement | null
          if (frm) {
            frm.action = '/tdab2/tdabs_external/AdminServlet_tchinese?cmd=cmdCheckIdentifierAction'
            frm.submit()
          }
        }
      })

      // 等待导航稳定（页面可能发生多次跳转）
      await Promise.race([
        page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: 30000 }),
        page.waitForURL(url => /AdminServlet_tchinese/.test(String(url)), { timeout: 30000 }).catch(() => {}) as any,
      ])
      await page.waitForLoadState('domcontentloaded', { timeout: 30000 }).catch(() => {})
      await this.randomDelay({ min: 0.3, max: 0.7 })
      const submitTime = Date.now() - verifyStartTime

      // 提交后无条件短等错误层出现（不阻塞正常流程），以便抓取完整 DOM
      try { await page.waitForSelector('#errTable, .errorlayer, .errorCode, #error_msg', { timeout: 2500 }) }
      catch {}

      let html: string
      try {
        const contentHtml = await page.content()
        let domHtml = ''
        try { domHtml = await page.evaluate(() => document.documentElement.outerHTML) }
        catch {}
        html = (typeof domHtml === 'string' && domHtml.length > (contentHtml?.length || 0) + 100) ? domHtml : contentHtml
      }
      catch (e: any) {
        const msg = String(e?.message || e)
        if (/navigating and changing the content/i.test(msg)) {
          await page.waitForLoadState('domcontentloaded', { timeout: 5000 }).catch(() => {})
          await this.randomDelay({ min: 0.2, max: 0.4 })
          const contentHtml = await page.content()
          let domHtml = ''
          try { domHtml = await page.evaluate(() => document.documentElement.outerHTML) }
          catch {}
          html = (typeof domHtml === 'string' && domHtml.length > (contentHtml?.length || 0) + 100) ? domHtml : contentHtml
        }
        else {
          throw e
        }
      }
      const pageType = this.parser.identifyPageType(html, page.url())

      const errorResult = this.parser.detectError(html)
      if (errorResult.success && errorResult.data) {
        // 立刻把当前 HTML 原样保存，避免再次读取导致与截图不一致
        try {
          if (this.config.logging.save_html) {
            await browser.saveHtml('04_identity_check', html)
            // 等待错误块出现后再抓一次完整 DOM（fallback，防止第一次尚未渲染完全）
            try { await page.waitForSelector('#errTable, .errorlayer, .errorCode, #error_msg', { timeout: 2000 }) }
            catch {}
            try {
              const dom = await page.evaluate(() => document.documentElement.outerHTML)
              if (typeof dom === 'string' && dom.length > (html?.length || 0) + 100)
                await browser.saveHtml('04_identity_check_error', dom)
            }
            catch {}
          }
        }
        catch {}
        const codeStr = String((errorResult.data as any)?.code || '').toUpperCase()
        const msgStr = String((errorResult.data as any)?.message || '').toLowerCase()
        const isRepeatLoginBlocked = codeStr.includes('512-E-0044') || /重覆登入|重复登入|短時間內不停刷新|短时间内不停刷新/.test(msgStr)
        if (isRepeatLoginBlocked) {
          // 命中 512-E-0044：拉黑当前代理并快速失败，由上层/外层重试用新代理
          try { this.browser?.banCurrentProxy?.('512-E-0044') }
          catch {}
          this.contextLogger?.warn('⛔ 命中 512-E-0044，已拉黑当前代理')
          return { success: false, error: '系统阻挡（512-E-0044）' }
        }

        if (errorResult.data.type === 'network_error') {
          this.contextLogger?.warn('检测到网络错误，尝试恢复', errorResult.data)
          const recoverOk = await this.recoverFromNetworkError(context)
          if (!recoverOk)
            return { success: false, error: `网络错误: ${errorResult.data.message}` }
          return await this.stepVerifyIdentity(context)
        }
        this.contextLogger?.error('身份验证出现错误', errorResult.data)
        return { success: false, error: `身份验证失败: ${errorResult.data.message} (${errorResult.data.code})` }
      }

      // 分支处理：电话号码补填页（SC-512-1-044）
      const needsPhoneNumber = html.includes('請輸入電話號碼') || html.includes('电话号码')
      if (needsPhoneNumber) {
        if (!userInfo.phone_number)
          return { success: false, error: '系统要求填写电话号码，请在用户信息中添加电话号码' }
        this.contextLogger?.info('📞 检测到电话补填页，填写并继续', { phone: userInfo.phone_number })
        await page.evaluate((phone) => {
          try {
            (window as any).isSubmitted = function () { return false }
          }
          catch {}
          const inp = document.getElementById('txtPhoneNumber') as HTMLInputElement | null
          if (inp)
            inp.value = phone
          if (typeof (window as any).onClickContinue === 'function')
            (window as any).onClickContinue()
        }, userInfo.phone_number)
        await Promise.race([
          page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: 30000 }),
          page.waitForURL(url => /AdminServlet_tchinese/.test(String(url)), { timeout: 30000 }).catch(() => {}) as any,
        ])
        await page.waitForLoadState('domcontentloaded', { timeout: 30000 }).catch(() => {})
        await this.randomDelay({ min: 0.3, max: 0.7 })
      }

      // 分支处理：选择牌照事务处页（SC-512-1-004）
      const needOffice = await page.evaluate(() => {
        return !!(document.querySelector('input[name="officeSelect"]') || document.getElementById('listLicenseCenter'))
      })
      if (needOffice) {
        const officeCode = this.config.booking.default_office_code
        this.contextLogger?.info('🏢 检测到事务处选择页', { 选择: officeCode })
        await page.evaluate((code) => {
          // 【关键】重置 isSubmitted 标记
          try {
            (window as any).isSubmitted = function () { return false }
          }
          catch {}
          const radios = document.querySelectorAll('input[name="officeSelect"]') as NodeListOf<HTMLInputElement>
          if (radios.length > 0) {
            const target = Array.from(radios).find(r => r.value === code) || radios[0]
            target.checked = true
            if (typeof (window as any).onClickSelectOffice === 'function') {
              (window as any).onClickSelectOffice(target.value)
              return
            }
          }
          const sel = document.getElementById('listLicenseCenter') as HTMLSelectElement | null
          if (sel) {
            sel.value = code
            if (typeof (window as any).onChangeOffice === 'function') {
              (window as any).onChangeOffice()
              return
            }
          }
          // 兜底：手动提交
          const frm = document.querySelector('form[name="frmAppointment"]') as HTMLFormElement | null
          if (frm) {
            let officeInput = frm.querySelector('input[name="storedAppointmentOfficeCode"]') as HTMLInputElement | null
            if (!officeInput) {
              officeInput = document.createElement('input')
              officeInput.type = 'hidden'
              officeInput.name = 'storedAppointmentOfficeCode'
              frm.appendChild(officeInput)
            }
            officeInput.value = code
            frm.action = '/tdab2/tdabs_external/AdminServlet_tchinese?cmd=cmdGetCalendarAction'
            frm.submit()
          }
        }, officeCode)
        await Promise.race([
          page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: 30000 }),
          page.waitForURL(url => /AdminServlet_tchinese/.test(String(url)), { timeout: 30000 }).catch(() => {}) as any,
        ])
        await page.waitForLoadState('domcontentloaded', { timeout: 30000 }).catch(() => {})
        await this.randomDelay({ min: 0.3, max: 0.7 })
      }

      if (this.config.logging.save_html)
        await browser.saveSnapshot('04_identity_check')

      this.contextLogger?.info(`✅ 身份验证完成`, {
        耗时: `${submitTime}ms`,
        分支: needsPhoneNumber ? '电话补填' : (needOffice ? '事务处选择' : '直接进入'),
        最终页面: pageType,
      })
      return { success: true, page_type: pageType, html_saved: undefined }
    }
    catch (e: unknown) {
      const message = e instanceof Error ? e.message : String(e)
      this.contextLogger?.error('❌ 身份验证失败', { 错误: message })
      return { success: false, error: `身份验证失败: ${message}` }
    }
  }

  /**
   * 网络错误自愈（当遇到"连接已重置"等网络错误时）
   * 三级恢复策略：
   * 1. reload 当前页（最轻量，保留会话）
   * 2. 切换入口回首页（换个服务器地址）
   * 3. 直连重启（禁用代理，排除代理问题）
   * @returns true 恢复成功，false 恢复失败
   */
  private async recoverFromNetworkError(context: BookingContext): Promise<boolean> {
    const browser = this.browser!
    const page = browser.getCurrentPage()!

    // 策略1：reload 当前页（最轻量，保留会话状态）
    try {
      await page.reload({ waitUntil: 'domcontentloaded', timeout: 15000 })
      await this.randomDelay({ min: 0.3, max: 0.8 })
      const html = await page.content()
      const err = this.parser.detectError(html)
      if (!err.success) {
        this.contextLogger?.info('网络错误恢复成功（reload）')
        return true
      }
    }
    catch {}

    // 策略2：切换入口重新访问首页（换服务器地址，保留会话）
    const bases = Array.isArray((this.config as any)?.network?.base_urls) && (this.config as any).network.base_urls.length > 0
      ? (this.config as any).network.base_urls as string[]
      : [this.config.network.base_url]
    for (const base of bases) {
      try {
        await page.goto(`${base}?cmd=cmdShowHome`, { waitUntil: 'domcontentloaded', timeout: 15000 })
        await this.randomDelay({ min: 0.3, max: 0.8 })
        const html = await page.content()
        const err = this.parser.detectError(html)
        if (!err.success) {
          this.contextLogger?.info('网络错误恢复成功（切换入口）', { base })
          return true
        }
      }
      catch {}
    }

    // 策略3：关闭浏览器并以无代理模式重启（排除代理问题）
    try {
      await browser.close()
      await browser.launch({ headless: context.options?.headless ?? true, timeout: context.options?.timeout ?? 30000, disableProxy: true } as any)
      const newPage = browser.getCurrentPage()!
      await newPage.goto(`${this.config.network.base_url}?cmd=cmdShowHome`, { waitUntil: 'domcontentloaded', timeout: 20000 })
      await this.randomDelay({ min: 0.3, max: 0.8 })
      this.contextLogger?.info('网络错误恢复成功（直连重启）')
      return true
    }
    catch (e) {
      this.contextLogger?.error('网络错误恢复失败', { error: e instanceof Error ? e.message : String(e) })
      return false
    }
  }

  /**
   * 步骤5：查询可用日期（步骤二：选择预约日期和时间）
   * 当前页：日历页（cmdGetCalendarAction）
   * 操作：
   *   1. 解析当前月份的可用日期（a[onclick*="onClickDate"]）
   *   2. 若当前月无可用日期，翻月继续查询（最多3个月）
   *   3. 翻月使用 onClickMonth(year, m0) 或手动设置隐藏域后 submit
   * 快照：每个月都保存 05_calendar_YYYY-MM
   * 防护：节流 900ms，频繁访问检测与冷却
   */
  private async stepQueryAvailableDates(context: BookingContext): Promise<StepResult> {
    this.throwIfCancelled()
    const browser = this.browser!
    const page = browser.getCurrentPage()!
    this.contextLogger?.info('开始查询可用日期（步骤二：选择预约日期和时间）')
    try {
      await page.waitForLoadState('domcontentloaded', { timeout: 10000 }).catch(() => {})
      await this.randomDelay({ min: 0.3, max: 0.6 })

      // 若仍未进入日历页，且停留在“选择牌照事务处”页面，则先完成事务处选择再进入日历
      try {
        const isCalendar = await page.evaluate(() => !!document.querySelector('a[onclick*="onClickDate"]'))
        if (!isCalendar) {
          const onOfficePage = await page.evaluate(() => !!(document.querySelector('input[name="officeSelect"]') || document.getElementById('listLicenseCenter')))
          if (onOfficePage) {
            const officeCode = this.config.booking.default_office_code
            this.contextLogger?.info('🏢 在查询日期前检测到事务处选择页，先选择事务处并进入日历', { 选择: officeCode })
            await page.evaluate((code) => {
              try { (window as any).isSubmitted = function () { return false } }
              catch {}
              const radios = document.querySelectorAll('input[name="officeSelect"]') as NodeListOf<HTMLInputElement>
              if (radios && radios.length > 0) {
                const target = Array.from(radios).find(r => r.value === code) || radios[0]
                target.checked = true
                if (typeof (window as any).onClickSelectOffice === 'function') {
                  (window as any).onClickSelectOffice(target.value)
                  return
                }
              }
              const sel = document.getElementById('listLicenseCenter') as HTMLSelectElement | null
              if (sel) {
                sel.value = code
                if (typeof (window as any).onChangeOffice === 'function') {
                  (window as any).onChangeOffice()
                  return
                }
              }
              // 兜底：直接写隐藏域并提交到日历
              const frm = document.querySelector('form[name="frmAppointment"]') as HTMLFormElement | null
              if (frm) {
                let officeInput = frm.querySelector('input[name="storedAppointmentOfficeCode"]') as HTMLInputElement | null
                if (!officeInput) {
                  officeInput = document.createElement('input'); officeInput.type = 'hidden'; officeInput.name = 'storedAppointmentOfficeCode'; frm.appendChild(officeInput)
                }
                officeInput.value = code
                frm.action = '/tdab2/tdabs_external/AdminServlet_tchinese?cmd=cmdGetCalendarAction'
                frm.submit()
              }
            }, officeCode)
            await Promise.race([
              page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: 30000 }),
              page.waitForURL(u => /cmd=cmdGetCalendarAction/i.test(String(u)), { timeout: 30000 }).catch(() => {}) as any,
            ])
            await page.waitForLoadState('domcontentloaded', { timeout: 30000 }).catch(() => {})
            await this.randomDelay({ min: 0.3, max: 0.6 })
          }
        }
      }
      catch {}

      // 仅扫描“今天起到窗口天数（默认70天）”所覆盖的月份；选择时支持会话传入的允许日期范围（首选日期优先，不受该范围限制）
      const windowDays = Math.max(1, Number(this.config.booking?.available_window_days || 70))
      const baseStart = new Date(); baseStart.setHours(0, 0, 0, 0)
      const baseEnd = new Date(baseStart.getTime() + windowDays * 24 * 60 * 60 * 1000)

      // 优先级：若预约数据设置“强制使用首选日期”，则不应用 allowed 范围，仅命中首选才继续
      const forcePreferred = !!((context as any)?.appointment?.force_preferred_dates)
      // 会话传入（前端 UI 设置）的允许日期范围，仅用于非首选日期的筛选（当非强制时）
      const allowed = forcePreferred ? undefined : (context.options as any)?.allowed_date_range as { start?: string, end?: string } | undefined
      const parseAllowed = (s?: string): Date | undefined => {
        if (!s || !String(s).trim())
          return undefined
        const v = String(s).trim()
        if (/^\d{4}-\d{2}-\d{2}$/.test(v)) {
          const [y, m, d] = v.split('-').map(Number)
          const dt = new Date(y, m - 1, d); dt.setHours(0, 0, 0, 0)
          return dt
        }
        try {
          const dt = parseDDMMYYYY(v)
          dt.setHours(0, 0, 0, 0)
          return dt
        }
        catch { return undefined }
      }
      const allowedStart = parseAllowed(allowed?.start)
      const allowedEnd = parseAllowed(allowed?.end)

      // 扫描范围：默认按基础窗口（月）
      let monthsToScan = buildMonthsRange(baseStart, baseEnd)
      // 优化：当允许范围起=止为同一天时，仅扫描该月；若为多天范围，则将包含允许范围的月份排在最前
      try {
        const unique = (arr: Array<{ y: number, m0: number }>) => {
          const seen = new Set<string>()
          const out: Array<{ y: number, m0: number }> = []
          for (const it of arr) {
            const k = `${it.y}-${it.m0}`
            if (!seen.has(k)) { seen.add(k); out.push(it) }
          }
          return out
        }
        const norm = (d?: Date) => d ? { y: d.getFullYear(), m0: d.getMonth() } : undefined
        if (allowedStart && allowedEnd) {
          if (allowedStart.getTime() === allowedEnd.getTime()) {
            const only = norm(allowedStart)!
            monthsToScan = [only]
          }
          else {
            const allowedMonths = buildMonthsRange(allowedStart, allowedEnd)
            monthsToScan = unique([...allowedMonths, ...monthsToScan])
          }
        }
      }
      catch {}
      const inBaseWindow = (dateStr: string): boolean => {
        try {
          const d = parseDDMMYYYY(dateStr)
          // 排除周六日
          const dow = d.getDay()
          if (dow === 0 || dow === 6)
            return false
          return d.getTime() >= baseStart.getTime() && d.getTime() <= baseEnd.getTime()
        }
        catch { return false }
      }
      const inAllowedRange = (dateStr: string): boolean => {
        try {
          const d = parseDDMMYYYY(dateStr)
          if (allowedStart && d.getTime() < allowedStart.getTime())
            return false
          if (allowedEnd && d.getTime() > allowedEnd.getTime())
            return false
          return true
        }
        catch { return false }
      }
      const inFinalWindow = (dateStr: string): boolean => inBaseWindow(dateStr) && inAllowedRange(dateStr)

      // 本地日期格式化（避免 toISOString 变为 UTC 导致日志日期减一天）
      const formatLocalYmd = (d?: Date) => {
        if (!d)
          return undefined
        const y = d.getFullYear()
        const m = String(d.getMonth() + 1).padStart(2, '0')
        const day = String(d.getDate()).padStart(2, '0')
        return `${y}-${m}-${day}`
      }

      this.contextLogger?.debug('📆 日期窗口', {
        起: formatLocalYmd(baseStart),
        止: formatLocalYmd(baseEnd),
        允许起: formatLocalYmd(allowedStart || undefined),
        允许止: formatLocalYmd(allowedEnd || undefined),
        覆盖月数: monthsToScan.length,
      })
      // 记录首选/强制首选设置，便于复盘
      try {
        this.contextLogger?.info('🎯 首选与筛选设置', {
          强制首选: forcePreferred,
          首选日期: (context as any)?.preferred_dates || [],
          允许范围: {
            起: formatLocalYmd(allowedStart || undefined),
            止: formatLocalYmd(allowedEnd || undefined),
          },
        })
      }
      catch {}

      // 辅助函数：解析当前日历页的年/月/可用日期（带一次稳态重试）
      const readCalendar = async (): Promise<{ year: number, m0: number, availableDates: string[] }> => {
        try {
          return await readCal(page)
        }
        catch (e: any) {
          const msg = String(e?.message || e)
          if (/Execution context was destroyed|navigating and changing the content/i.test(msg)) {
            await page.waitForLoadState('domcontentloaded', { timeout: 5000 }).catch(() => {})
            await this.randomDelay({ min: 0.2, max: 0.4 })
            return await readCal(page)
          }
          throw e
        }
      }

      // 辅助函数：翻月到指定年月
      // 参数：year 年份，m0 月份索引（0-11）
      // 优先 onClickMonth，兜底手动设置隐藏域并 submit
      const gotoMonth = async (year: number, m0: number) => {
        await this.ensureMinInterval('calendar-nav', 600)
        await gotoCalMonth(page, year, m0)
        // 等待由 onClickMonth/表单提交触发的导航稳定
        await Promise.race([
          page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: 30000 }),
          page.waitForURL(url => /cmd=cmdGetCalendarAction/i.test(String(url)), { timeout: 30000 }).catch(() => {}) as any,
        ])
        await page.waitForLoadState('domcontentloaded', { timeout: 30000 }).catch(() => {})
        // 等待隐藏域反映目标年月，避免仍处于上一月而触发 evaluate 销毁
        await page.waitForFunction((p: { y: number, m: number }) => {
          const yEl = document.querySelector('input[name="storedAppointmentCalendarYear"]') as HTMLInputElement | null
          const mEl = document.querySelector('input[name="storedAppointmentCalendarMonth"]') as HTMLInputElement | null
          if (!yEl || !mEl) { return false }
          return Number(yEl.value) === p.y && Number(mEl.value) === p.m
        }, { y: year, m: m0 }, { timeout: 5000 }).catch(() => {})
        await this.randomDelay({ min: 0.3, max: 0.6 })
      }

      // DEBUG：强制成功路径（直接选择窗口内首日）
      if (this.isDebugForceSuccess()) {
        const startDate = new Date(); startDate.setHours(0, 0, 0, 0)
        const pickedD = String(startDate.getDate()).padStart(2, '0')
        const pickedM = String(startDate.getMonth() + 1).padStart(2, '0')
        const pickedY = startDate.getFullYear()
        const fakeDate = `${pickedD}-${pickedM}-${pickedY}`
        context.selected_date = fakeDate
        this.contextLogger?.warn('⚠️ DEBUG_FORCE_SUCCESS: 直接选择伪造可用日期', { 日期: fakeDate })
        return { success: true, page_type: 'calendar', data: { available_dates: [], selected_date: fakeDate }, html_saved: undefined }
      }

      // 主流程：仅扫描窗口覆盖的月份
      let picked: string | null = null
      let current = await readCalendar()
      // 额外保护：若允许范围为单日，优先强制对齐到该月，避免偶发停留在当前月导致误扫
      if (allowedStart && allowedEnd && allowedStart.getTime() === allowedEnd.getTime()) {
        const onlyY = allowedStart.getFullYear()
        const onlyM0 = allowedStart.getMonth()
        if (!(current.year === onlyY && current.m0 === onlyM0)) {
          await gotoMonth(onlyY, onlyM0)
          current = await readCalendar()
        }
      }
      // 若当前展示月份不在窗口首月，则先跳到窗口首月
      if (!(current.year === monthsToScan[0].y && current.m0 === monthsToScan[0].m0)) {
        await gotoMonth(monthsToScan[0].y, monthsToScan[0].m0)
        current = await readCalendar()
      }
      // 在扫描过程中累计“窗口内候选日期”，用于后续时段为空的回退
      const allWindowCandidates: string[] = []
      for (let i = 0; i < monthsToScan.length; i++) {
        // 先解析摘要（更稳健地获取年月，避免出现 NaN 年/月）
        let calSummary: any = null
        try { calSummary = await analyzeCalendarSummary(page) }
        catch {}
        const yLog = Number.isFinite(current.year) ? current.year : (calSummary?.year ?? 'unknown')
        const mLog = Number.isFinite(current.m0) ? (current.m0 + 1) : ((calSummary?.monthIndex0 ?? 0) + 1)

        // 保存当前月的日历快照（文件名使用更稳健的年月）
        const ym = `${yLog || 'unknownY'}-${String(mLog).padStart(2, '0')}`
        if (this.config.logging.save_html)
          await browser.saveSnapshot(`05_calendar_${ym}`)
        this.contextLogger?.info(`📅 扫描 ${yLog}年${mLog}月`, { 可用日期数: current.availableDates.length, 第几轮: i + 1 })
        // 列举当前月网页可约日期，便于快速判断“放了哪些天”
        try { this.contextLogger?.info('🗓️ 网页可约日期', { 月: ym, 日期: current.availableDates }) }
        catch {}

        // 输出“日历解析完成”统计摘要
        if (calSummary) {
          this.contextLogger?.info(`📆 ${calSummary.year}年${calSummary.monthIndex0 + 1}月日历解析完成: 总计 ${calSummary.daysInMonth} 天, 可预约 ${calSummary.availableCount} 天, 休息日 ${calSummary.restCount} 天, 约满 ${calSummary.bookedCount} 天, 暂不可约 ${calSummary.tempCount} 天`)
        }

        // 频繁访问检测：若页面被重定向到"访问过于频繁"页面，需冷却等待
        const htmlForErr = await page.content()
        const errInfo = this.parser.detectError(htmlForErr)
        if (errInfo.success && errInfo.data && errInfo.data.type === 'frequent_access') {
          this.contextLogger?.warn('⚠️ 触发频繁访问限制，冷却 5-8s', { code: errInfo.data.code })
          if (this.config.logging.save_html)
            await browser.saveSnapshot('05_calendar_rate_limited')
          await this.cooldown({ min: 5, max: 8 })
        }

        // 过滤：先计算配置范围内的窗口日期（合并“随机放号模拟/调试注入”以便非监控模式测试）
        let effectiveAvailableDates = Array.isArray(current.availableDates) ? current.availableDates.slice() : []
        const ymStr = `${yLog || 'unknownY'}-${String(mLog).padStart(2, '0')}`
        const addedMockDates: string[] = []
        try {
          const nodeEnv = String(process.env.NODE_ENV || '').toLowerCase()
          const debugEnabled = nodeEnv !== 'production'
          const useRandomRelease = debugEnabled && String(process.env.DEBUG_MONITOR_RANDOM_RELEASE || '').trim() === '1'
          let debugMockDates: string[] = []
          if (debugEnabled) {
            const envMock = String(process.env.DEBUG_MONITOR_MOCK_DATES || '').trim()
            if (envMock)
              debugMockDates = envMock.split(/[;,\s]+/).map(s => s.trim()).filter(Boolean)
            // 兼容 YYYY-MM-DD 与 DD-MM-YYYY，两者统一转换为 DD-MM-YYYY
            if (debugMockDates.length > 0) {
              const toDMY = (v: string) => {
                const s = String(v || '').trim()
                const m = s.match(/^(\d{4})-(\d{2})-(\d{2})$/)
                return m ? `${m[3]}-${m[2]}-${m[1]}` : s
              }
              debugMockDates = debugMockDates.map(toDMY)
            }
          }
          // 合并 mock dates（仅当前月且在基础窗口内）
          if (debugEnabled && debugMockDates.length > 0) {
            const set = new Set<string>(effectiveAvailableDates)
            for (const d of debugMockDates) {
              try {
                const dt = parseDDMMYYYY(d)
                if (dt.getFullYear() === current.year && dt.getMonth() === (current.m0 || 0) && inBaseWindow(d) && ![0, 6].includes(dt.getDay())) {
                  if (!set.has(d))
                    addedMockDates.push(d)
                  set.add(d)
                }
              }
              catch {}
            }
            effectiveAvailableDates = Array.from(set)
            if (addedMockDates.length > 0)
              this.contextLogger?.info('🧪 调试: 注入模拟日期（Mock）', { 月: ymStr, 日期: addedMockDates })
          }
          // 合并随机放号（覆盖未来窗口月份）
          if (useRandomRelease) {
            const gen = generateRandomRelease({ startDate: baseStart, endDate: baseEnd, year: current.year, month0: current.m0 || 0 })
            const set = new Set<string>(effectiveAvailableDates)
            for (const d of gen.availableDates)
              set.add(d)
            effectiveAvailableDates = Array.from(set)
            if (gen.availableDates.length > 0)
              this.contextLogger?.info('🧪 调试: 随机放号（Random）', { 月: ymStr, 日期: gen.availableDates, 配额: gen.quotas })
          }
        }
        catch {}
        const windowDates = (effectiveAvailableDates || []).filter(inFinalWindow)
        for (const d of windowDates)
          allWindowCandidates.push(d)
        // 记录合并/过滤后的可选日期，区分窗口与全量
        try {
          this.contextLogger?.info('📜 可选日期（合并后）', { 月: ymStr, 全量: effectiveAvailableDates, 窗口内: windowDates })
        }
        catch {}

        // 首选日期优先匹配（不受会话允许范围限制，但仍需在基础窗口与当前页有）—兼容 YYYY-MM-DD 与 DD-MM-YYYY
        const toDMY = (v: string) => {
          const s = String(v || '').trim()
          if (/^\d{2}-\d{2}-\d{4}$/.test(s))
            return s
          const m = s.match(/^(\d{4})-(\d{2})-(\d{2})$/)
          if (m)
            return `${m[3]}-${m[2]}-${m[1]}`
          return s
        }
        if (context.preferred_dates && context.preferred_dates.length > 0) {
          const prefDMY = context.preferred_dates.map(toDMY)
          // 关键修复：使用合并后的 effectiveAvailableDates（包含模拟/随机注入）
          const hit = prefDMY.find(d => (effectiveAvailableDates || []).includes(d) && inBaseWindow(d))
          if (hit) { picked = hit; break }
          if (forcePreferred) {
            // 强制模式：本月未命中首选，翻到下一个月继续找，直到窗口结束；若最终未命中则失败
          }
        }
        // 非强制：否则选配置范围内“一个可用日期”
        // 为提升命中真实可用时段的概率，支持通过环境变量控制选取顺序（默认选最早；DEBUG_BOOKING_PICK_DESC=1 选最晚）
        if (!forcePreferred && windowDates.length > 0) {
          try {
            const pickDesc = process.env.DEBUG_BOOKING_PICK_DESC === '1'
            const sorted = windowDates.slice().sort((a, b) => {
              const da = parseDDMMYYYY(a).getTime()
              const db = parseDDMMYYYY(b).getTime()
              return pickDesc ? (db - da) : (da - db)
            })
            picked = sorted[0]
          }
          catch {
            picked = windowDates[0]
          }
          break
        }

        // 当前月窗口内无可用日期，若还有下一个窗口月则翻至下一窗口月
        if (i + 1 < monthsToScan.length) {
          const next = monthsToScan[i + 1]
          await gotoMonth(next.y, next.m0)
          current = await readCalendar()
        }
      }

      if (!picked)
        return { success: false, error: forcePreferred ? '未命中首选日期（强制）' : '当前没有可用的预约日期' }

      // 记录候选日期（供“空时段回退”使用）
      try { (context as any).candidate_dates = Array.from(new Set(allWindowCandidates)) }
      catch {}
      context.selected_date = picked
      this.contextLogger?.info(`✅ 找到可用日期`, { 日期: picked, 扫描月数: current.m0 !== undefined ? (current.m0 + 1) : '未知' })
      return { success: true, page_type: 'calendar', data: { available_dates: [], selected_date: picked }, html_saved: undefined }
    }
    catch (e: unknown) {
      const message = e instanceof Error ? e.message : String(e)
      this.contextLogger?.error('❌ 查询可用日期失败', { 错误: message })
      return { success: false, error: `查询可用日期失败: ${message}` }
    }
  }

  /**
   * 步骤6：选择时段
   * 当前页：日历页（cmdGetCalendarAction）
   * 操作：调用 onClickDate(date) 进入时段列表页
   * 目标页：时段页（cmdGetTimeslotListAction）
   * 解析：a.redlink[onclick*="onClickTimeslot"] 提取所有可用时段
   * 快照：06_timeslot
   */
  private async stepSelectTimeslot(context: BookingContext): Promise<StepResult> {
    this.throwIfCancelled()
    const browser = this.browser!
    const page = browser.getCurrentPage()!
    if (!context.selected_date)
      return { success: false, error: '未选择预约日期' }
    this.contextLogger?.info('开始查询可用时段', { selectedDate: context.selected_date })
    try {
      // DEBUG：强制成功路径（构造伪造时段）
      if (this.isDebugForceSuccess()) {
        const fakeSlot: TimeslotInfo = { id: 'FAKE-1000-1100', display_time: '10:00-11:00', start_time: '1000', end_time: '1100' } as any
        context.selected_timeslot = { ...fakeSlot, date: context.selected_date }
        this.contextLogger?.warn('⚠️ DEBUG_FORCE_SUCCESS: 直接选择伪造时段', { 时间: fakeSlot.display_time })
        return { success: true, page_type: 'timeslot', data: { available_timeslots: [fakeSlot], selected_timeslot: fakeSlot }, html_saved: undefined }
      }
      // 调用 onClickDate 进入时段列表页
      await page.evaluate((date) => {
        if (typeof (window as any).onClickDate === 'function') {
          (window as any).onClickDate(date)
        }
        else {
          const frm = document.querySelector('form[name="frmAppointment"]') as HTMLFormElement | null
          if (frm) {
            let dateInput = frm.querySelector('input[name="storedAppointmentTimeslotDate"]') as HTMLInputElement | null
            if (!dateInput) {
              dateInput = document.createElement('input')
              dateInput.type = 'hidden'
              dateInput.name = 'storedAppointmentTimeslotDate'
              frm.appendChild(dateInput)
            }
            dateInput.value = date
            frm.action = '/tdab2/tdabs_external/AdminServlet_tchinese?cmd=cmdGetTimeslotListAction'
            frm.submit()
          }
        }
      }, context.selected_date)
      // 等待由 onClickDate/表单提交触发的导航稳定到时段页
      await Promise.race([
        page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: 30000 }),
        page.waitForURL(u => /cmd=cmdGetTimeslotListAction/i.test(String(u)), { timeout: 30000 }).catch(() => {}) as any,
        page.waitForSelector('a[onclick*="onClickTimeslot"]', { timeout: 30000 }).catch(() => {}),
      ])
      await page.waitForLoadState('domcontentloaded', { timeout: 30000 }).catch(() => {})
      await this.randomDelay({ min: 0.3, max: 0.7 })

      // 读取 HTML（如仍遇到导航中错误则轻量补等重试一次）
      let timeslotHtml: string
      try {
        timeslotHtml = await page.content()
      }
      catch (e: any) {
        const msg = String(e?.message || e)
        if (/navigating and changing the content|Execution context was destroyed/i.test(msg)) {
          await page.waitForLoadState('domcontentloaded', { timeout: 5000 }).catch(() => {})
          await this.randomDelay({ min: 0.2, max: 0.4 })
          timeslotHtml = await page.content()
        }
        else {
          throw e
        }
      }
      const pageType = this.parser.identifyPageType(timeslotHtml, page.url())
      this.contextLogger?.debug('时段页面加载完成', { pageType, currentUrl: page.url() })

      const errorResult = this.parser.detectError(timeslotHtml)
      if (errorResult.success && errorResult.data)
        return { success: false, error: `时段页面错误: ${errorResult.data.message} (${errorResult.data.code})` }

      // 解析时段列表
      let timeslotsResult = this.parser.parseAvailableTimeslots(timeslotHtml)
      if (this.config.logging.save_html)
        await browser.saveSnapshot('06_timeslot')
      if (!timeslotsResult.success || !timeslotsResult.data || timeslotsResult.data.length === 0) {
        this.contextLogger?.warn('该日期没有可用时段，尝试回退到其它允许日期', { 日期: context.selected_date })
        const candidates: string[] = Array.isArray((context as any)?.candidate_dates) ? (context as any).candidate_dates : []
        const remaining = candidates.filter(d => d !== context.selected_date)

        const backToCalendar = async () => {
          await page.evaluate(() => {
            const doSubmit = () => {
              const frm = document.querySelector('form[name="frmAppointment"]') as HTMLFormElement | null
              if (!frm) { return }
              frm.action = '/tdab2/tdabs_external/AdminServlet_tchinese?cmd=cmdGetCalendarAction'
              try { frm.submit() }
              catch {}
            }
            if (typeof (window as any).onClickBack === 'function') {
              try { (window as any).onClickBack(); return }
              catch {
                try { doSubmit() }
                catch {}
                return
              }
            }
            try { doSubmit() }
            catch {}
          })
          await Promise.race([
            page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: 30000 }),
            page.waitForURL(u => /cmd=cmdGetCalendarAction/i.test(String(u)), { timeout: 30000 }).catch(() => {}) as any,
          ])
          await page.waitForLoadState('domcontentloaded', { timeout: 30000 }).catch(() => {})
          await this.randomDelay({ min: 0.3, max: 0.7 })
        }

        const loadTimeslotsFor = async (d: string) => {
          await page.evaluate((date) => {
            if (typeof (window as any).onClickDate === 'function') {
              (window as any).onClickDate(date)
            }
            else {
              const frm = document.querySelector('form[name="frmAppointment"]') as HTMLFormElement | null
              if (frm) {
                const ensure = (n: string, v: string) => {
                  let el = frm!.querySelector(`input[name="${n}"]`) as HTMLInputElement | null
                  if (!el) { el = document.createElement('input'); el.type = 'hidden'; el.name = n; frm!.appendChild(el) }
                  el.value = v
                }
                ensure('appointmentDate', date)
                ;(frm as any).action = '/tdab2/tdabs_external/AdminServlet_tchinese?cmd=cmdGetTimeslotListAction'
                try { frm.submit() }
                catch {}
              }
            }
          }, d)
          await Promise.race([
            page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: 30000 }),
            page.waitForURL(u => /cmd=cmdGetTimeslotListAction/i.test(String(u)), { timeout: 30000 }).catch(() => {}) as any,
            page.waitForSelector('a[onclick*="onClickTimeslot"]', { timeout: 30000 }).catch(() => {}) as any,
          ])
          await page.waitForLoadState('domcontentloaded', { timeout: 30000 }).catch(() => {})
          await this.randomDelay({ min: 0.3, max: 0.7 })
          let html2: string
          try { html2 = await page.content() }
          catch { await this.randomDelay({ min: 0.2, max: 0.4 }); html2 = await page.content() }
          const r = this.parser.parseAvailableTimeslots(html2)
          if (this.config.logging.save_html)
            await browser.saveSnapshot('06_timeslot')
          return r
        }

        for (const alt of remaining) {
          try {
            await backToCalendar()
            const r = await loadTimeslotsFor(alt)
            if (r.success && r.data && r.data.length > 0) {
              context.selected_date = alt
              timeslotsResult = r as any
              break
            }
          }
          catch {}
        }

        if (!timeslotsResult || !timeslotsResult.success || !timeslotsResult.data || timeslotsResult.data.length === 0)
          return { success: false, error: `${context.selected_date} 该日期没有可用时段` }
      }
      // 列举该日所有可预约时段
      try {
        this.contextLogger?.info('🕒 可预约时段清单', {
          日期: context.selected_date,
          时段: timeslotsResult.data.map(s => s.display_time),
          数量: timeslotsResult.data.length,
        })
      }
      catch {}

      // 选择策略：优先匹配用户首选时间，再按策略排序（earliest|latest|random|hash）。默认 preference_first_hash。
      const orderCandidates = (list: TimeslotInfo[]): TimeslotInfo[] => {
        const slots = list.slice()
        const preferred: TimeslotInfo[] = []
        const nonPreferred: TimeslotInfo[] = []
        const hasPref = Array.isArray(context.preferred_times) && context.preferred_times.length > 0
        const matchPref = (t: TimeslotInfo): boolean => {
          if (!hasPref)
            return false
          const start = (t.start_time || '').toString()
          return context.preferred_times!.some(pt => t.display_time.includes(pt) || start === String(pt).replace(':', ''))
        }
        for (const t of slots) {
          if (matchPref(t))
            preferred.push(t)
          else nonPreferred.push(t)
        }

        const toMinutes = (s?: string): number => {
          if (!s)
            return 24 * 60
          const m = String(s).trim().replace(':', '')
          const hh = Number(m.slice(0, 2))
          const mm = Number(m.slice(2))
          if (!Number.isFinite(hh) || !Number.isFinite(mm))
            return 24 * 60
          return hh * 60 + mm
        }
        const strategy = String(((this.config as any)?.booking?.timeslot_selection_strategy
          || process.env.DEBUG_BOOKING_TIMESLOT_STRATEGY
          || 'preference_first_hash')).toLowerCase()

        const shuffle = (arr: TimeslotInfo[]) => {
          for (let i = arr.length - 1; i > 0; i--) {
            const j = Math.floor(Math.random() * (i + 1))
            const tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp
          }
          return arr
        }
        const hash32 = (s: string): number => {
          let h = 2166136261 >>> 0
          for (let i = 0; i < s.length; i++) {
            h ^= s.charCodeAt(i)
            h = Math.imul(h, 16777619) >>> 0
          }
          return h >>> 0
        }

        const orderByStrategy = (arr: TimeslotInfo[]): TimeslotInfo[] => {
          const copy = arr.slice()
          if (strategy.includes('earliest')) {
            copy.sort((a, b) => toMinutes(a.start_time) - toMinutes(b.start_time))
            return copy
          }
          if (strategy.includes('latest')) {
            copy.sort((a, b) => toMinutes(b.start_time) - toMinutes(a.start_time))
            return copy
          }
          if (strategy.includes('random')) {
            return shuffle(copy)
          }
          if (strategy.includes('hash')) {
            if (copy.length <= 1)
              return copy
            const key = `${(context.user_info?.passport_number || '')}|${(context.user_info?.license_number || '')}|${context.selected_date || ''}`
            const idx = hash32(key) % copy.length
            const [pick] = copy.splice(idx, 1)
            return [pick, ...copy]
          }
          // 默认 earliest
          copy.sort((a, b) => toMinutes(a.start_time) - toMinutes(b.start_time))
          return copy
        }

        const ordered = [...preferred, ...orderByStrategy(nonPreferred)]
        return ordered
      }

      const orderedCandidates = orderCandidates(timeslotsResult.data)
      // 保存候选顺序，供后续 TIMESLOT_FULL 回退时沿用既定顺序，降低冲突
      try { (context as any).timeslot_candidates = orderedCandidates.map(s => s.id) }
      catch {}

      const selectedTimeslot: TimeslotInfo = orderedCandidates[0]

      context.selected_timeslot = { ...selectedTimeslot, date: context.selected_date }
      // 已选择具体时段后再保存一次快照便于复盘
      if (this.config.logging.save_html)
        await browser.saveSnapshot('06_timeslot_selected')

      this.contextLogger?.info(`✅ 时段选择成功`, {
        时间: selectedTimeslot.display_time,
        可用时段数: timeslotsResult.data.length,
        是否首选: context.preferred_times?.some(t => selectedTimeslot.display_time.includes(t)) || false,
      })
      return { success: true, page_type: pageType, data: { available_timeslots: timeslotsResult.data, selected_timeslot: selectedTimeslot }, html_saved: undefined }
    }
    catch (e: unknown) {
      const message = e instanceof Error ? e.message : String(e)
      this.contextLogger?.error('❌ 选择时段失败', { 错误: message })
      return { success: false, error: `选择时段失败: ${message}` }
    }
  }

  /**
   * 步骤7：确认预约（步骤三：遞交預約資料）
   * 当前页：时段页（cmdGetTimeslotListAction）
   * 操作：
   *   1. 调用 onClickTimeslot(id, date, start, end) 进入确认页
   *   2. 在确认页调用 onClickSubmit() 提交最终预约
   * 目标页：确认页（cmdConfirmAppointment）→ 结果页（cmdSubmitAppointmentAction）
   * 快照：07_confirm（确认页）、08_final_result（结果页）
   * 成功标志：结果页包含"參考編號"
   */
  private async stepConfirmBooking(context: BookingContext): Promise<StepResult> {
    this.throwIfCancelled()
    const browser = this.browser!
    const page = browser.getCurrentPage()!
    if (!context.selected_timeslot)
      return { success: false, error: '未选择预约时段' }
    this.contextLogger?.info('开始确认预约（步骤三：遞交預約資料）', { timeslot: context.selected_timeslot.display_time, timeslotId: context.selected_timeslot.id, date: context.selected_date })
    try {
      // DEBUG：强制成功路径（直接生成参考编号并返回成功）
      if (this.isDebugForceSuccess()) {
        const ref = `FAKE-${Date.now().toString().slice(-6)}`
        if (!context.session_data)
          (context as any).session_data = {}
        ;(context.session_data as any).reference_number = ref
        this.contextLogger?.warn('⚠️ DEBUG_FORCE_SUCCESS: 直接模拟预约成功', {
          参考编号: ref,
          日期: context.selected_date,
          时间: context.selected_timeslot?.display_time,
        })
        return { success: true, page_type: 'result', data: { success: true, reference_number: ref, message: '预约成功(模拟)' }, html_saved: undefined }
      }
      // 调用 onClickTimeslot 进入确认页
      await page.evaluate((slot) => {
        // 优先调用页面函数
        if (typeof (window as any).onClickTimeslot === 'function') {
          (window as any).onClickTimeslot(slot.id, slot.date, slot.start_time, slot.end_time)
        }
        else {
          // 兜底：手动设置隐藏域并提交表单
          const frm = document.querySelector('form[name="frmAppointment"]') as HTMLFormElement | null
          if (frm) {
            const ensure = (n: string, v: string) => {
              let el = frm!.querySelector(`input[name="${n}"]`) as HTMLInputElement | null
              if (!el) {
                el = document.createElement('input')
                el.type = 'hidden'
                el.name = n
                frm!.appendChild(el)
              }
              el.value = v
            }
            ensure('storedAppointmentTimeslotId', slot.id)
            ensure('storedAppointmentTimeslotDate', slot.date)
            ensure('storedAppointmentTimeslotFromTime', slot.start_time)
            ensure('storedAppointmentTimeslotToTime', slot.end_time)
            frm.action = '/tdab2/tdabs_external/AdminServlet_tchinese?cmd=cmdConfirmAppointment'
            frm.submit()
          }
        }
      }, context.selected_timeslot)
      // 等待由 onClickTimeslot/表单提交触发的导航稳定到“确认页”
      await Promise.race([
        page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: 30000 }),
        page.waitForURL(u => /cmd=cmdConfirmAppointment/i.test(String(u)), { timeout: 30000 }).catch(() => {}) as any,
      ])
      await page.waitForLoadState('domcontentloaded', { timeout: 30000 }).catch(() => {})
      await this.randomDelay({ min: 0.3, max: 0.7 })

      let confirmHtml: string
      try {
        confirmHtml = await page.content()
      }
      catch (e: any) {
        const msg = String(e?.message || e)
        if (/navigating and changing the content|Execution context was destroyed/i.test(msg)) {
          await page.waitForLoadState('domcontentloaded', { timeout: 5000 }).catch(() => {})
          await this.randomDelay({ min: 0.2, max: 0.4 })
          confirmHtml = await page.content()
        }
        else {
          throw e
        }
      }
      const confirmPageType = this.parser.identifyPageType(confirmHtml, page.url())

      const confirmErrorResult = this.parser.detectError(confirmHtml)
      if (confirmErrorResult.success && confirmErrorResult.data)
        return { success: false, error: `确认页面错误: ${confirmErrorResult.data.message} (${confirmErrorResult.data.code})` }

      if (this.config.logging.save_html)
        await browser.saveSnapshot('07_confirm')

      this.contextLogger?.debug('📋 进入确认页，准备提交最终预约', { pageType: confirmPageType })
      // 提交前短暂延时（模拟用户核对信息）
      await this.randomDelay(this.config.booking.human_like_delays.pre_submit_delay)

      // JS 注入：提交最终预约
      await page.evaluate(() => {
        // 优先调用页面函数
        if (typeof (window as any).onClickSubmit === 'function') {
          (window as any).onClickSubmit()
        }
        else {
          // 次优：查找"繼續/提交"按钮并点击
          const btn = Array.from(document.querySelectorAll('input,button')).find((b) => {
            const v = ((b as HTMLInputElement).value || b.textContent || '').trim()
            return /確認|继续|繼續|提交/.test(v)
          })
          if (btn) {
            (btn as HTMLElement).click()
          }
          else {
            // 兜底：直接提交表单
            const frm = document.querySelector('form[name="frmAppointment"]') as HTMLFormElement | null
            if (frm) {
              frm.action = '/tdab2/tdabs_external/AdminServlet_tchinese?cmd=cmdSubmitAppointmentAction'
              frm.submit()
            }
          }
        }
      })
      await Promise.race([
        page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: 30000 }),
        page.waitForURL(u => /cmd=cmdSubmitAppointmentAction/i.test(String(u)), { timeout: 30000 }).catch(() => {}) as any,
      ])
      await page.waitForLoadState('domcontentloaded', { timeout: 30000 }).catch(() => {})
      await this.randomDelay({ min: 0.4, max: 0.9 })

      // 等待结果页加载
      let resultHtml: string
      try {
        resultHtml = await page.content()
      }
      catch (e: any) {
        const msg = String(e?.message || e)
        if (/navigating and changing the content|Execution context was destroyed/i.test(msg)) {
          await page.waitForLoadState('domcontentloaded', { timeout: 5000 }).catch(() => {})
          await this.randomDelay({ min: 0.2, max: 0.4 })
          resultHtml = await page.content()
        }
        else {
          throw e
        }
      }
      const resultType = this.parser.identifyPageType(resultHtml, page.url())

      // 解析预约结果（提取参考编号或错误信息）
      const bookingResult = this.parser.parseBookingResult(resultHtml)
      if (this.config.logging.save_html)
        await browser.saveSnapshot('08_final_result')

      if (bookingResult.success && bookingResult.data?.success) {
        // 预约成功：保存参考编号到 session_data
        if (bookingResult.data.reference_number) {
          if (!context.session_data)
            (context as any).session_data = {};
          (context.session_data as any).reference_number = bookingResult.data.reference_number
          this.contextLogger?.info('🎉 预约成功！', {
            参考编号: bookingResult.data.reference_number,
            日期: context.selected_date,
            时间: context.selected_timeslot?.display_time,
            姓名: context.user_info.license_owner_name,
          })
        }
        return { success: true, page_type: resultType, data: bookingResult.data, html_saved: undefined }
      }
      else {
        // 预约失败：如命中“時段經已額滿（512-E-0003）”，回退到日历并尝试同日其它时段
        const errCode = bookingResult.data?.error_code || ''
        const errMsg = bookingResult.data?.message || '预约提交失败'
        if (String(errCode).toUpperCase() === 'TIMESLOT_FULL') {
          this.contextLogger?.warn('⚠️ 时段已满，准备回退重新选择其它时段', { 日期: context.selected_date, 原时段: context.selected_timeslot?.display_time })

          const backToCalendar = async () => {
            await page.evaluate(() => {
              const doSubmit = () => {
                const frm = document.querySelector('form[name="frmAppointment"]') as HTMLFormElement | null
                if (!frm) { return }
                frm.action = '/tdab2/tdabs_external/AdminServlet_tchinese?cmd=cmdGetCalendarAction'
                try { frm.submit() }
                catch {}
              }
              if (typeof (window as any).onClickBack === 'function') {
                try { (window as any).onClickBack(); return }
                catch {
                  try { doSubmit() }
                  catch {}; return
                }
              }
              try { doSubmit() }
              catch {}
            })
            await Promise.race([
              page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: 30000 }),
              page.waitForURL(u => /cmd=cmdGetCalendarAction/i.test(String(u)), { timeout: 30000 }).catch(() => {}) as any,
            ])
            await page.waitForLoadState('domcontentloaded', { timeout: 30000 }).catch(() => {})
            await this.randomDelay({ min: 0.3, max: 0.7 })
            this.contextLogger?.info('🔙 回退：已返回日历')
          }

          const openTimeslotsFor = async (d: string) => {
            await page.evaluate((date) => {
              if (typeof (window as any).onClickDate === 'function') {
                (window as any).onClickDate(date)
              }
              else {
                const frm = document.querySelector('form[name="frmAppointment"]') as HTMLFormElement | null
                if (frm) {
                  let dateInput = frm.querySelector('input[name="storedAppointmentTimeslotDate"]') as HTMLInputElement | null
                  if (!dateInput) {
                    dateInput = document.createElement('input')
                    dateInput.type = 'hidden'
                    dateInput.name = 'storedAppointmentTimeslotDate'
                    frm.appendChild(dateInput)
                  }
                  dateInput.value = date
                  frm.action = '/tdab2/tdabs_external/AdminServlet_tchinese?cmd=cmdGetTimeslotListAction'
                  frm.submit()
                }
              }
            }, d)
            await Promise.race([
              page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: 30000 }),
              page.waitForURL(u => /cmd=cmdGetTimeslotListAction/i.test(String(u)), { timeout: 30000 }).catch(() => {}) as any,
              page.waitForSelector('a[onclick*="onClickTimeslot"]', { timeout: 30000 }).catch(() => {}),
            ])
            await page.waitForLoadState('domcontentloaded', { timeout: 30000 }).catch(() => {})
            await this.randomDelay({ min: 0.3, max: 0.7 })
            let html2: string
            try { html2 = await page.content() }
            catch { await this.randomDelay({ min: 0.2, max: 0.4 }); html2 = await page.content() }
            try {
              if (this.config.logging.save_html) {
                const suffix = String(d || '').replace(/\D/g, '') || 'retry'
                await browser.saveSnapshot(`06_timeslot_retry_${suffix}`)
              }
            }
            catch {}
            return this.parser.parseAvailableTimeslots(html2)
          }

          try {
            await backToCalendar()
            const r = await openTimeslotsFor(context.selected_date!)
            if (r.success && r.data && r.data.length > 0) {
              const prevId = context.selected_timeslot?.id
              let candidates = r.data.filter(s => s.id !== prevId)
              // 若有既定候选顺序，按顺序重排，减少多人同时撞同一时段
              try {
                const orderIds: string[] = Array.isArray((context as any)?.timeslot_candidates) ? (context as any).timeslot_candidates : []
                if (orderIds.length > 0) {
                  const rank = new Map<string, number>()
                  orderIds.forEach((id, i) => rank.set(id, i))
                  candidates = candidates.slice().sort((a, b) => (rank.get(a.id) ?? 9999) - (rank.get(b.id) ?? 9999))
                }
              }
              catch {}
              this.contextLogger?.info('🕒 回退可预约时段清单', { 日期: context.selected_date, 数量: candidates.length, 时段: candidates.map(s => s.display_time) })
              let retryIndex = 0
              for (const alt of candidates) {
                try {
                  retryIndex += 1
                  this.contextLogger?.info('🔁 回退尝试其它时段', { 次序: retryIndex, 日期: context.selected_date, 时段: alt.display_time, 时段ID: alt.id })
                  // 进入确认页
                  await page.evaluate((slot) => {
                    if (typeof (window as any).onClickTimeslot === 'function') {
                      (window as any).onClickTimeslot(slot.id, slot.date, slot.start_time, slot.end_time)
                    }
                    else {
                      const frm = document.querySelector('form[name="frmAppointment"]') as HTMLFormElement | null
                      if (frm) {
                        const ensure = (n: string, v: string) => {
                          let el = frm!.querySelector(`input[name="${n}"]`) as HTMLInputElement | null
                          if (!el) { el = document.createElement('input'); el.type = 'hidden'; el.name = n; frm!.appendChild(el) }
                          el.value = v
                        }
                        ensure('storedAppointmentTimeslotId', slot.id)
                        ensure('storedAppointmentTimeslotDate', slot.date)
                        ensure('storedAppointmentTimeslotFromTime', slot.start_time)
                        ensure('storedAppointmentTimeslotToTime', slot.end_time)
                        frm.action = '/tdab2/tdabs_external/AdminServlet_tchinese?cmd=cmdConfirmAppointment'
                        frm.submit()
                      }
                    }
                  }, { ...alt, date: context.selected_date! })
                  await Promise.race([
                    page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: 30000 }),
                    page.waitForURL(u => /cmd=cmdConfirmAppointment/i.test(String(u)), { timeout: 30000 }).catch(() => {}) as any,
                  ])
                  await page.waitForLoadState('domcontentloaded', { timeout: 30000 }).catch(() => {})
                  await this.randomDelay({ min: 0.3, max: 0.7 })
                  try {
                    if (this.config.logging.save_html) {
                      await browser.saveSnapshot(`07_confirm_retry_${retryIndex}`)
                    }
                  }
                  catch {}

                  // 提交
                  await page.evaluate(() => {
                    if (typeof (window as any).onClickSubmit === 'function') {
                      (window as any).onClickSubmit()
                    }
                    else {
                      const frm = document.querySelector('form[name="frmAppointment"]') as HTMLFormElement | null
                      if (frm) {
                        frm.action = '/tdab2/tdabs_external/AdminServlet_tchinese?cmd=cmdSubmitAppointmentAction'; try { frm.submit() }
                        catch {}
                      }
                    }
                  })
                  await Promise.race([
                    page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: 30000 }),
                    page.waitForURL(u => /cmd=cmdSubmitAppointmentAction/i.test(String(u)), { timeout: 30000 }).catch(() => {}) as any,
                  ])
                  await page.waitForLoadState('domcontentloaded', { timeout: 30000 }).catch(() => {})
                  await this.randomDelay({ min: 0.3, max: 0.7 })
                  const html3 = await page.content()
                  try {
                    if (this.config.logging.save_html) {
                      await browser.saveSnapshot(`08_final_result_retry_${retryIndex}`)
                    }
                  }
                  catch {}
                  const br = this.parser.parseBookingResult(html3)
                  if (br.success && br.data?.success && br.data?.reference_number) {
                    if (!context.session_data)
                      (context as any).session_data = {}
                    ;(context.session_data as any).reference_number = br.data.reference_number
                    this.contextLogger?.info('🎉 预约成功（回退重试）！', { 参考编号: br.data.reference_number, 日期: context.selected_date, 时间: alt.display_time })
                    return { success: true, page_type: 'result', data: br.data, html_saved: undefined }
                  }
                  // 若仍为“时段已满”，则继续尝试下一个候选；其它错误直接返回
                  const derr = this.parser.detectError(html3)
                  if (!(derr.success && derr.data && String(derr.data.code).toUpperCase() === 'TIMESLOT_FULL')) {
                    return { success: false, error: br.data?.message || '预约提交失败' }
                  }
                  else {
                    this.contextLogger?.warn('⛔ 回退尝试仍为满员，继续下一候选', { 次序: retryIndex, 日期: context.selected_date, 时段: alt.display_time, 错误码: derr.data?.code })
                  }
                }
                catch {}
              }
              // 所有候选均已占用
              return { success: false, error: '所有候选时段均已占用，请稍后再试' }
            }
          }
          catch {}

          // 回退失败：返回原错误
          this.contextLogger?.error('❌ 预约失败（回退尝试后仍失败）', { 错误: errMsg, 错误码: errCode })
          return { success: false, error: errMsg, html_saved: undefined }
        }

        // 其它错误：直接返回
        this.contextLogger?.error('❌ 预约失败', { 错误: errMsg, 错误码: errCode })
        return { success: false, error: errMsg, html_saved: undefined }
      }
    }
    catch (e: unknown) {
      const message = e instanceof Error ? e.message : String(e)
      this.contextLogger?.error('❌ 确认预约失败', { 错误: message })
      return { success: false, error: `确认预约失败: ${message}` }
    }
  }

  /**
   * 随机延时（模拟人类操作）
   * 会对配置中的延时范围做上限裁剪（最多 0.4-0.8s），避免过慢
   */
  private async randomDelay(range: { min: number, max: number }): Promise<void> {
    const min = Math.min(range.min, 0.4)
    const max = Math.max(min, Math.min(range.max, 0.8))
    const delay = (min + Math.random() * (max - min)) * 1000
    await this.sleepCancellable(delay)
  }

  /**
   * 确保两次操作间最小间隔（节流）
   * 用于防止高频操作触发站点"频繁访问"限制（如日历翻月）
   */
  private async ensureMinInterval(key: string, minIntervalMs: number): Promise<void> {
    const now = Date.now()
    const last = this.lastActionAt[key] || 0
    const need = last + minIntervalMs - now
    if (need > 0)
      await this.sleepCancellable(need)
    this.lastActionAt[key] = Date.now()
  }

  /**
   * 冷却等待（触发频繁访问限制后）
   * 等待 5-8s 让站点解除限制
   */
  private async cooldown(range: { min: number, max: number }): Promise<void> {
    const ms = (range.min + Math.random() * (range.max - range.min)) * 1000
    await this.sleepCancellable(ms)
  }

  /**
   * 可中断睡眠：每 80ms 检查一次取消标记，尽快响应 stop
   */
  private async sleepCancellable(ms: number): Promise<void> {
    let remaining = Math.max(0, Math.floor(ms))
    const slice = 80
    while (remaining > 0) {
      if (this.cancelled)
        return
      const seg = Math.min(slice, remaining)
      await new Promise(r => setTimeout(r, seg))
      remaining -= seg
    }
  }

  /**
   * DEBUG：是否启用强制成功（用于无可用日期环境下的联调/测试）。
   * 开关：
   * - 环境变量 DEBUG_BOOKING_FORCE_SUCCESS=1
   */
  private isDebugForceSuccess(): boolean {
    try {
      return process.env.DEBUG_BOOKING_FORCE_SUCCESS === '1'
    }
    catch {
      return false
    }
  }

  // =====================
  // 对外复用：供“监控优先”直接复用引擎步骤
  // =====================
  /**
   * 仅执行导航到“日历页”的完整步骤（首页→条款→个人信息→身份验证/选事务处），返回是否已到达日历。
   * 注意：该方法会负责启动浏览器，但不会自动关闭，调用方需在结束后调用 close()。
   */
  public async openToCalendar(context: BookingContext): Promise<{ success: boolean }> {
    this.cancelled = false
    this.progress = 0
    this.traceId = generateTraceId()
    this.sessionId = `booking-${Date.now()}-${Math.random().toString(36).substr(2, 6)}`

    // 初始化上下文日志记录器
    this.contextLogger = createTrackedContextLogger({ component: 'BookingEngine', sessionId: this.sessionId, userName: context.user_info.license_owner_name, passport: context.user_info.passport_number }, this.traceId)

    // 运行目录
    const runDir = await ensureRunDirectory('booking', { name: context.user_info.license_owner_name, passportTail: (context.user_info.passport_number || '').slice(-4) || 'xxxx' }, false)

    const browser = new BookingBrowserManager()
    this.browser = browser
    try { browser.setRunDirectory(runDir) }
    catch {}
    await browser.launch({ headless: context.options?.headless ?? true, timeout: context.options?.timeout ?? 30000 } as any)

    // 顺序执行四个步骤直到日历
    const s1 = await this.stepNavigateHome(context)
    if (!s1.success)
      return { success: false }
    const s2 = await this.stepAgreeTerms(context)
    if (!s2.success)
      return { success: false }
    const s3 = await this.stepFillPersonalInfo(context)
    if (!s3.success)
      return { success: false }
    const s4 = await this.stepVerifyIdentity(context)
    if (!s4.success)
      return { success: false }

    // 判断是否已在日历
    try {
      const page = this.browser!.getCurrentPage()!
      const html = await page.content()
      const pageType = this.parser.identifyPageType(html, page.url())
      const isCalendar = typeof pageType === 'string' ? /calendar|cmdGetCalendarAction/i.test(pageType) : /cmdGetCalendarAction/i.test(page.url())
      if (isCalendar)
        return { success: true }
    }
    catch {}
    return { success: true } // 身份验证成功通常已进入日历
  }

  /** 获取当前 Page（供监控器在日历页解析） */
  public getPage(): any { return this.browser?.getCurrentPage() || null }
  public getBrowserManager(): BookingBrowserManager | null { return this.browser }
}

// 单例模式：全局唯一的预约引擎实例
let bookingEngineForBooking: BookingEngineForBooking | null = null

/**
 * 获取预约引擎实例（单例）
 */
export function getBookingEngineForBooking(): BookingEngineForBooking {
  if (!bookingEngineForBooking)
    bookingEngineForBooking = new BookingEngineForBooking()
  return bookingEngineForBooking
}
